feat(installer): add omitTypeField knob to the mcp-json family (Windsurf, #952) - #1
Conversation
Adds spec-driven custom installer targets so agents without a built-in target (opencode forks like CoDev Code colbymchenry#1272, Codex-shaped TOML agents like Grok Build colbymchenry#1324) can be wired up with a small declarative spec instead of a registry PR. - Extract the opencode and codex targets into family factories (opencode-family.ts, toml-family.ts) — behavior-preserving, pinned by the existing 162-test installer contract suite. Add a third generic mcp-json family for standard mcpServers-JSON agents. - targets/custom.ts: spec schema + validation (id shape, reserved words, builtin collisions, path-shape rules), tolerant loader (invalid specs skip with a warning, never crash the installer), strict add/remove persistence to ~/.codegraph/targets.json (CODEGRAPH_TARGETS_FILE override). - Registry merges customs after built-ins; --target <id>/auto/all, the interactive multiselect, uninstall's sweep, and --print-config all see them with no further changes. - CLI: codegraph targets add|list|remove. - 30 new tests in __tests__/custom-targets.test.ts; design doc at docs/design/custom-installer-targets.md; CHANGELOG entry.
Custom specs can now carry post-install notes the orchestrator surfaces verbatim (the existing WriteResult.notes channel, same as Cursor's 'Restart Cursor to apply') — for agent quirks the user must act on, e.g. Windsurf not reloading mcp_config.json until the MCP panel's Refresh is hit. Install-only (including idempotent re-runs); never written into any agent file; validated to ≤5 single-line strings. Suggested by @SEPURI-SAI-KRISHNA in colbymchenry#1459 review discussion (the Windsurf refresh gotcha from colbymchenry#952).
…specs Two knobs for GUI-IDE agents, both from Qoder requirements confirmed by its target-PR author in colbymchenry#1459 discussion (colbymchenry#1277): - configDir (toml/mcp-json families) now accepts a per-platform {darwin, win32, linux} map, and either form may start with one leading ${ENV} token (${APPDATA}/... — Windows has no ~ for %APPDATA%-rooted dirs). Unresolvable on this machine (no entry for the platform, env var unset) degrades to not-installed / a 'cannot locate config dir' note — never a crash. - absoluteCommand (mcp-json family) writes the codegraph binary's resolved absolute path as the entry command, for Dock/Finder-launched macOS apps whose stripped PATH can't find nvm-managed binaries. Reuses the built-in Antigravity target's resolver, now extracted to targets/shared.ts (antigravity.ts imports it; behavior unchanged). expandHomeDir moved from toml-family.ts into the new spec-paths.ts alongside the platform-map/env-token resolution.
quickbeard
left a comment
There was a problem hiding this comment.
Reviewed and tested locally — this is clean and I'd like to take it. Thank you for turning it around so fast.
Scope is exactly right. omitTypeField mirrors absoluteCommand one-for-one: boolean, mcp-json family only (with the same validation guard and error message), default false, so every existing spec serializes byte-identically. No existing target changes shape.
Correctness checks I ran, all good:
serverEntry()mutating thendelete entry.typeis safe —getMcpServerConfig()returns a fresh object per call, so there's no shared-state aliasing (same reasonabsoluteCommand'sentry.command = …is safe).- Idempotency holds across a toggle: installing without the knob writes
{type, command, args}; re-installing the same target withomitTypeField: truemakesafter = {command, args}, sojsonDeepEqual(before, after)is false → one cleanupdated, not a spuriousunchanged. And uninstall deletes thecodegraphkey regardless of entry shape, so it still reverses. printConfigreflects the knob for free because it already routes throughserverEntry()— your second test pins that.- Windsurf's global-only behavior falls out correctly from the spec omitting
localConfigDir(family returnssupportsLocation('local') === false), so no local-install surprise.
Tests: the 3 you added match the existing absoluteCommand / notes patterns exactly (validation guard, drop-vs-keep, printConfig shape). I ran the full installer set on a merge of this onto the branch tip: 208 passing, tsc clean.
One tiny optional note, non-blocking: a spec that sets both absoluteCommand and omitTypeField isn't covered by a test, but they're independent one-liners in serverEntry() and each is tested alone, so I'm not worried.
Heads-up on mechanics: I've just rebased colbymchenry#1459 onto latest main (75 commits had landed). To spare you a rebase of this PR onto the moved tip, I'll bring your commit in via cherry-pick with your authorship preserved and reference it here — you'll show up as the commit author on colbymchenry#1459. Flagging so the "closed" status on this PR reads as "included," not "rejected." Thanks again — this is the piece that makes Windsurf fully spec-expressible.
d01359c to
b657797
Compare
…ame-collision matches (CG-10, colbymchenry#1500) Explore's per-file relevance awarded +50/+10/+3/+1 by match class and admitted anything scoring >= 3. Neither half held up: the tier said HOW a symbol reached us, never whether the match was evidence, and an absolute floor admits noise on any repo where the top file scores 50+. Three scripts/agent-eval/*.mjs harnesses took 63% of this repo's own "how does explore allocate its output budget" answer on nothing but an unused `const explore` and a `const BUDGET`. Four levers: - KIND WEIGHT (RELEVANCE_KIND_WEIGHT): callables and types 1.0, members ~0.5, variable/constant/parameter 0.15-0.35. A weak-kind symbol with no usage edge anywhere in the graph (`contains` excluded — nesting is not usage) drops to 0.08. Only weak kinds in the top two tiers pay for the DB probe; the subgraph's own edges answer most cases free. No measurable latency change (210 vs 211 ms/call, n=12 interleaved). - PERIPHERAL CAP: nodes >=2 hops from any match accumulate into a bucket capped at 5. Uncapped they added a flat +1 each, so a file grew more relevant by being bigger — parse-session.mjs reached 22 off one constant plus twelve unrelated symbols. - RANK PENALTY: generated files x0.3, low-value x0.5, applied to the score AND the graph mass. Score alone would not have fixed colbymchenry#1500 — the generated CRUD carries MORE graph mass than the hand-written use-case, and graph mass outranks score in the comparator. Self-normalizing, never a hard exclusion. - RELATIVE FLOOR: clamp(topScore * 0.2, 1, 10). Capped at one full-strength direct match so concentration elsewhere can never exclude one (without it a named-seed-heavy file pushed the floor to 21 and dropped a file the agent had named by class name). Backfills to 3 candidates when it would leave fewer, and drops the evidence requirement rather than return nothing at all. excludeLowValueFiles was dead config — declared per tier, read nowhere; the test/spec exclusion has been unconditional for a while. Removed. The real gap was the detector: `isLowValue` anchored on a leading `/`, so a repo-ROOT `test/` dir (express, cobra, most of npm and Go) never matched — express's routing question spent 59% of its envelope on three test files. Anchored at `^` too, and the filter now runs before the floor and judges "are there other candidates?" on the whole gather. Measured before/after on the same indexes (baseline bd86ad2): - payroll-go fixture: generated 57.4% -> 23.5%; answer 25.6% -> 61.5%; cycle.go delivered 0 -> 38.9%. Generated ranks colbymchenry#3/colbymchenry#4, was #1/colbymchenry#2. - self-query fixture: eval scripts 72% -> 0%; tools.ts ranks #1. - express "route a request": 59% to test/* -> lib/application.js + lib/response.js - cobra x3, codegraph "indexing pipeline": byte-identical (control) Diagnostic gains a per-file penalty multiplier and NodeKind mix, so "why did this file score X" is legible. Selection stages reordered to match the pipeline. CG-6's gates flip from it.fails to live regressions except the byte-split ones, which stay open for CG-12 (allocation still follows file size within the ranked set). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…(CG-27) CG-27 asked whether the >50%-of-file envelope drop should cover `function` / `method`, so a `createFoo()` factory returning an object of closures stops merging every closure inside it into one cluster. Measured on a hermetic fixture, it should not, and the issue is closed as obsolete with CG-30 credited. Two mechanisms already absorb the shape. shrinkCluster orders members by (importance desc, size ASC) and refuses any member that overruns the cap once something is kept, so a file-spanning member is only selected when it is the sole member of the top importance tier — eight of nine query shapes never selected it at all. When it IS selected, CG-30 windows it on whole lines, so the file still delivers bounded, readable source (6 of 9 closure definitions in that configuration). Dropping the range instead SPLITS the file, and only the first-chosen cluster may be shrunk: a trivial 7-line cluster won the density tiebreak and the answer-bearing cluster was dropped whole — rank-#1 file 7,539 chars and 7 of 11 closures to 397 and none. Reaching the same intent more carefully (defer the envelope MEMBER inside shrinkCluster, leaving clustering untouched) is noise: 69 vs 68 closure definitions across nine query shapes. Nothing shipped. Adds the fixture, the probe, a standing gate on the outcome, and the record — including a real defect the measurement exposed on the epic tip: django's query.py leaves 8,212 of 10,135 unspent and drops a score-290 cluster to keep a score-14 one. Filed separately. No behaviour change, so no CHANGELOG entry.
CG-27 proposed adding function/method to ENVELOPE_KINDS so a factory closure spanning most of its file stops merging every inner symbol into one cluster. The issue required the ranking claim be measured before any fix. It was, on a hermetic fixture built to make the pattern maximally visible, and it does not hold — nothing shipped to src/. The literal change is a large regression: dropping the enclosing range SPLITS the file into a trivial cluster (a type alias plus a helper, span 7) and the answer-bearing one (every closure, span 359). Cluster ranking breaks the equal maxImportance tie on density, so the trivial cluster wins, is taken first, and is the only one that may be shrunk; the answer-bearing cluster then does not fit and is dropped whole. Rank #1 fell from 7,539 delivered chars to 397, and from 7 of 11 inner closures to 0. The enclosing range was holding the file together as one cluster, inside which shrinkCluster already did the per-symbol ranking the issue asked for. A better mechanism reaching the same intent — deferring the envelope member inside shrinkCluster, leaving clustering untouched — is noise: 69 vs 68 inner definitions across nine query shapes, one better, one worse, seven unchanged. The one configuration where the envelope IS selected (the factory as sole top-tier member) is already absorbed by CG-30, which windows it on whole lines: a contiguous readable head carrying 6 of 9 closures, bounded and never empty. Kept: the fixture, the deterministic probe, and the measurement record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A file that declares nothing but types and that nothing in the index depends on — a hand-written ambient `.d.ts` of global shims, vendored typings, module augmentation — cannot answer a flow question: no bodies, no call edges, no behaviour, nothing typed by it. But the identifiers it declares are exactly the generic ones a prose question uses (`Body`, `Message`, `ImageMetadata`, `ReadableStream`), so on term overlap it out-scored the implementation. Measured on the new fixture: rank #1 and 51% of delivered source, with the flow's own entry file pushed out of the response entirely. Measured first, per the issue: the Wrangler `worker-configuration.d.ts` that opened this is already handled by CG-25's banner detection, worth 15-46 points of envelope share across four flow queries. CG-25 credited; only the un-bannered case needed anything. `rankPenalty` now multiplies score and graph mass by 0.5 for such files, taken as the STRONGER of it and the generated penalty rather than multiplied — one property two signals see must not be charged twice. Detection is structural, not by extension, and four conditions deep. Two of them were forced by measurement: requiring every symbol to be type-level takes the corpus flag rate from 1-18% (which swept in Kotlin sealed classes, Rust mod.rs re-exports and django's locale tables) down to 0-4%; requiring that nothing depends on the file separates an ambient shim from a working types module, and without it the rule demoted displacement-ts's pipeline `types.ts` and broke the CG-31 gate. A query that NAMES a declared type is exempt, so a question about a type still reaches its declaration at full weight. Precise tokens only, so "…the file body…" cannot exempt a `Body` interface it never meant to name; this needs its own set because `namedSeedIds` is callable-only and a type never becomes one. Regression evidence in docs/benchmarks/explore-declaration-only-cg28.md: 6-repo envelope sweep byte-identical against a clean baseline build, zero ambient files reach the candidate set on VS Code across five queries, corpus flag rate 0-0.74%, both allocation fixtures PASS, full suite 2,978 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…CG-28) Both halves of the issue were measured on a hermetic fixture of four declaration-shaped files varying on banner and depended-on-ness. CG-25 already handles the motivating file: the Wrangler worker-configuration.d.ts that opened this issue is demoted by the generated penalty alone, worth 15-46 points of envelope share across four flow queries. No new mechanism for it. The narrower gap is real. A declaration file with NO banner carried pen 1.00, took rank #1 and 51% of delivered source on a prose flow query, and displaced the flow's own entry file out of the response entirely. The rule is deliberately narrow, and both conditions were derived by survey rather than guessed. 'Declares no callable and calls nothing' flags 1.1-18.0% of files across the corpus and catches real source — okhttp's SocketPolicy.kt, tokio/src/runtime/mod.rs, Alamofire's umbrella file, django's locale format tables. Requiring every symbol to be type-level drops that to 0-4%. The 'nothing depends on it' condition was added after the broader version demoted a pure-interface file with 13 inbound imports and broke the CG-31 displacement gate — a different invariant entirely. Does NOT stack with the generated penalty: rankPenalty takes Math.min of the two, so a file that is both takes the stronger, never the product. A query that NAMES a declaration symbol exempts its file entirely, so asking about a type still reaches it at full weight. Six-repo envelope is byte-identical to the pre-change tip — the rule does not fire on any benchmark repo, consistent with the 0-4% survey. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Included in the branch as b657797 — your commit, your authorship, cherry-picked verbatim on top of the rebase onto latest For anyone landing here later, the Windsurf spec: { "id": "windsurf", "displayName": "Windsurf", "family": "mcp-json",
"configDir": "~/.codeium/windsurf", "configFileName": "mcp_config.json",
"instructionsFileName": null, "omitTypeField": true,
"notes": ["In Windsurf, open the MCP panel and click Refresh to load codegraph."] } |
Follow-up to your invitation on colbymchenry#1459. Adds the one knob the mcp-json family was missing for Windsurf.
What: an omitTypeField?: boolean option on the mcp-json family. When set, the server entry is written as { command, args } with no type: "stdio" key — the shape Windsurf's docs specify. Scoped exactly like your absoluteCommand knob: boolean, mcp-json family only, default false, so every existing spec is byte-identical.
Why: Windsurf (colbymchenry#952) rejects the extra type key. With this knob, Windsurf is fully expressible as a spec — no built-in needed:
Tests: +3 in custom-targets.test.ts (validation guard; drops type when set / keeps type:"stdio" by default; printConfig shape). Full run green: custom-targets 43/43, installer-targets 162/162.